package jcurses.widgets;

import java.util.ArrayList;

import jcurses.event.ValueChangedEvent;
import jcurses.event.ValueChangedListener;
import jcurses.event.ValueChangedListenerManager;
import jcurses.system.CharColor;
import jcurses.system.InputChar;
import jcurses.system.Toolkit;
import jcurses.util.Paging;
import jcurses.util.Rectangle;

/**
 * The class is the superclass for text editing widgets
 */
public class TextComponent extends Widget implements IWritable {

	@SuppressWarnings("unused")
	private static String escapeString(String text) {
		StringBuffer buf = new StringBuffer();
		for (int i = 0; i < text.length(); i++) {
			char c = text.charAt(i);
			if (c == '\n') {
				buf.append("\\n");
			} else if (c == '\r') {
				buf.append("\\r");
			} else if (c == '\t') {
				buf.append("\\t");
			} else {
				buf.append(c);
			}

		}

		return buf.toString();

	}

	private FocusData focus = new FocusData();
	private int _width = 0;

	private int _height = 0;
	private int _cursPosX = 0;

	private int _cursPosY = 0;
	private int _firstChar = 0;

	private int _firstLine = 0;
	StringBuffer _text = new StringBuffer("");
	ArrayList<Integer> _lines = new ArrayList<Integer>();

	ArrayList<Integer> _lineLengths = new ArrayList<Integer>();

	private static CharColor __textComponentDefaultColors = new CharColor(
			CharColor.MAGENTA, CharColor.BLACK);

	private static CharColor __focusedTextComponentDefaultColors = new CharColor(
			CharColor.BLUE, CharColor.WHITE, CharColor.REVERSE);

	private CharColor _focusedTextComponentColors = getFocusedTextComponentDefaultColors();

	private static CharColor __cursorDefaultColors = new CharColor(
			CharColor.BLACK, CharColor.WHITE);

	private CharColor _cursorColors = getCursorDefaultColors();

	private ValueChangedListenerManager _listenerManager = new ValueChangedListenerManager();

	/**
	 * Constructor with no arguments. No preferred width and height, empty.
	 */

	public TextComponent() {
		this(-1, -1, "");
	}

	/**
	 * The constructor
	 * 
	 * @param width
	 *            the preferred width of the component. If -1 is stated, there
	 *            is no preferred width and the component is layouted dependend
	 *            on the container and the text
	 * @param height
	 *            the preferred height of the component. If -1 is stated, there
	 *            is no preferred width and the component is layouted dependend
	 *            on the container and the text.
	 * 
	 */
	public TextComponent(int width, int height) {
		this(width, height, "");
	}

	/**
	 * The constructor
	 * 
	 * @param width
	 *            the preferred width of the component. If -1 is stated, there
	 *            is no preferred width and the component is layouted dependend
	 *            on the container and the text
	 * @param height
	 *            the preferred height of the component. If -1 is stated, there
	 *            is no preferred width and the component is layouted dependend
	 *            on the container.
	 * @text the initial text, if <code>null<code> the component is empty
	 * 
	 */

	public TextComponent(int width, int height, String text) {
		_width = width;
		_height = height;
		setText(text);
	}

	/**
	 * Konstruktor, nur Text, h�he und Breite vom Container bestimmt
	 */

	public TextComponent(String text) {
		this(-1, -1, text);
	}

	public void addListener(ValueChangedListener listener) {
		_listenerManager.addListener(listener);
	}

	public CharColor getCursorColors() {
		return _cursorColors;
	}

	public CharColor getCursorDefaultColors() {
		return __cursorDefaultColors;
	}

	public CharColor getDefaultColors() {
		return __textComponentDefaultColors;
	}

	@Override
	public void getFocus() {
		this.focus.focus = true;
		changeColors();
		drawCursor();
	}

	public CharColor getFocusedTextComponentColors() {
		return _focusedTextComponentColors;
	}

	public CharColor getFocusedTextComponentDefaultColors() {
		return __focusedTextComponentDefaultColors;
	}

	/**
	 * @return contained text
	 */
	public String getText() {
		return _text.toString();
	}

	public boolean handleInput(InputChar ch) {

		int bCursorPosX = _cursPosX;
		int bCursorPosY = _cursPosY;
		int bFirstChar = _firstChar;
		int bFirstLine = _firstLine;
		char bChar = getCharacterAtCursorPosition();

		if (ch.getCode() == InputChar.KEY_RIGHT) {
			setCursorLocation(_cursPosX + 1, _cursPosY);
			redrawAfterCursorMove(bCursorPosX, bCursorPosY, bFirstChar,
					bFirstLine, bChar);
			return true;
		} else if (ch.getCode() == InputChar.KEY_LEFT) {
			setCursorLocation(_cursPosX - 1, _cursPosY);
			redrawAfterCursorMove(bCursorPosX, bCursorPosY, bFirstChar,
					bFirstLine, bChar);
			return true;
		} else if (ch.getCode() == InputChar.KEY_UP) {
			setCursorLocation(_cursPosX, _cursPosY - 1);
			redrawAfterCursorMove(bCursorPosX, bCursorPosY, bFirstChar,
					bFirstLine, bChar);
			return true;
		} else if (ch.getCode() == InputChar.KEY_DOWN) {
			setCursorLocation(_cursPosX, _cursPosY + 1);
			redrawAfterCursorMove(bCursorPosX, bCursorPosY, bFirstChar,
					bFirstLine, bChar);
			return true;
		} else if (ch.getCode() == InputChar.KEY_HOME) {
			setCursorLocation(_cursPosX, 0);
			redrawAfterCursorMove(bCursorPosX, bCursorPosY, bFirstChar,
					bFirstLine, bChar);
			return true;
		} else if (ch.getCode() == InputChar.KEY_END) {
			setCursorLocation(_cursPosX, getTextHeight() - 1);
			redrawAfterCursorMove(bCursorPosX, bCursorPosY, bFirstChar,
					bFirstLine, bChar);
			return true;
		} else if (ch.getCode() == InputChar.KEY_NPAGE) {
			int newYPos = 0;
			if (getCurrentPageNumber() < (getPageSize() - 1)) {
				newYPos = getPaging().getIndexByPageOffset(
						getCurrentPageNumber() + 1, getCurrentPageOffset());
			} else {
				newYPos = getTextHeight() - 1;
			}
			setCursorLocation(_cursPosX, newYPos, true);
			redrawAfterCursorMove(bCursorPosX, bCursorPosY, bFirstChar,
					bFirstLine, bChar);
			return true;
		} else if (ch.getCode() == InputChar.KEY_PPAGE) {
			int newYPos = 0;
			if (getCurrentPageNumber() > 0) {
				newYPos = getPaging().getIndexByPageOffset(
						getCurrentPageNumber() - 1, getCurrentPageOffset());
			} else {
				newYPos = 0;
			}
			setCursorLocation(_cursPosX, newYPos, true);
			redrawAfterCursorMove(bCursorPosX, bCursorPosY, bFirstChar,
					bFirstLine, bChar);
		} else if (ch.getCode() == InputChar.KEY_BACKSPACE) {
			deleteCharBeforeCursorLocation();
			if (_cursPosX == 0) {
				if (_cursPosY > 0) {
					int y = _cursPosY - 1;
					if (y < 0) {
						y = 0;
					}
					int x = ((Integer) _lineLengths.get(y)).intValue();
					setCursorLocation(x, y);
					redrawAfterTextChange(bCursorPosX, bCursorPosY, bFirstChar,
							bFirstLine);
				}
			} else {
				setCursorLocation(_cursPosX - 1, _cursPosY);
				redrawLine(_cursPosY);
			}

			return true;
		} else if (!ch.isSpecialCode()) {
			char c = ch.getCharacter();
			insertCharAtCursorLocation(c);
			if (c == '\n') {
				setCursorLocation(0, _cursPosY + 1);
				redrawAfterTextChange(bCursorPosX, bCursorPosY, bFirstChar,
						bFirstLine);
			} else {
				setCursorLocation(_cursPosX + 1, _cursPosY);
				redrawLine(_cursPosY);
			}
			return true;
		}

		return false;
	}

	@Override
	public boolean hasFocus() {
		return this.focus.focus;
	}

	@Override
	public boolean isFocusable() {
		return this.focus.focusable;
	}

	@Override
	public void lostFocus() {
		this.focus.focus = false;
		changeColors();
		redrawOldChar(_cursPosX, _cursPosY, getCharacterAtCursorPosition());
	}

	public void removeListener(ValueChangedListener listener) {
		_listenerManager.removeListener(listener);
	}

	public void setCursorColors(CharColor colors) {
		_cursorColors = colors;
	}

	/**
	 * The method sets the cursor position to given koordinates ( within the
	 * text, not widget )
	 * 
	 * @param x
	 *            new x cursor coordinate within the text
	 * @param y
	 *            new y cursor coordinate within the text
	 */
	public void setCursorLocation(int x, int y) {
		setCursorLocation(x, y, false);
	}

	@Override
	public void setFocus(boolean value) {
		if (value) {
			getFocus();
		} else {
			lostFocus();
		}
	}

	@Override
	public void setFocusable(boolean focusable) {
		this.focus.focusable = focusable;
	}

	/**
	 * Sets the conteined text
	 * 
	 * @param text
	 *            text to set
	 */

	public void setText(String text) {
		setText(text, true);
	}

	/**
	 * Sets the contained text
	 * 
	 * @param text
	 *            text to set
	 * @param dispatchEvent
	 *            if true, the widget is repainted
	 */
	public void setText(String text, boolean dispatchEvent) {
		text = (text == null) ? "" : text;
		_text = new StringBuffer(text);
		updateText(dispatchEvent);
		reset();

	}

	public void setTextComponentColors(CharColor colors) {
		_focusedTextComponentColors = colors;
	}

	private void changeColors() {
		Rectangle rect = getTextRectangle();
		CharColor colors = hasFocus() ? getFocusedTextComponentColors()
				: getColors();
		Toolkit.changeColors(rect, colors);
	}

	private void deleteCharBeforeCursorLocation() {
		int pos = ((Integer) _lines.get(_cursPosY)).intValue() + _cursPosX;
		if (pos > 0) {
			_text.deleteCharAt(pos - 1);
		}
		updateText();
	}

	private void drawBox() {
		drawBox(getTextRectangle());
	}

	private void drawBox(Rectangle rect) {
		CharColor colors = hasFocus() ? getFocusedTextComponentColors()
				: getColors();
		Toolkit.drawRectangle(rect, colors);
	}

	private void drawChar(int x, int y, CharColor colors, char c) {
		int x1 = x - _firstChar;
		int y1 = y - _firstLine;
		String toPrint = (c == 0) ? " " : replaceTextLineForPrinting("" + c);
		Toolkit.printString(toPrint, getTextRectangle().getX() + x1,
				getTextRectangle().getY() + y1, colors);
	}

	private void drawCursor() {
		char c = getCharacterAtCursorPosition();
		drawChar(_cursPosX, _cursPosY, getCursorColors(), c);
	}

	private void drawLine(int index) {
		Rectangle rect = getTextRectangle();
		CharColor colors = hasFocus() ? getFocusedTextComponentColors()
				: getColors();
		int firstLine = getFirstLineNumber();
		int firstChar = getFirstCharNumber();
		int pos = ((Integer) _lines.get(index)).intValue();
		int length = ((Integer) _lineLengths.get(index)).intValue();
		if ((firstChar < length) && ((index - firstLine) < rect.getHeight())) {
			int length2 = length - firstChar;
			int length3 = (length2 > rect.getWidth()) ? rect.getWidth()
					: length2;
			Toolkit.printString(replaceTextLineForPrinting(_text.substring(pos
					+ firstChar, pos + firstChar + length3)), rect.getX(),
					rect.getY() + index - firstLine, colors);
		}
	}

	private void drawText() {
		drawText(0);
	}

	private void drawText(int index) {
		int firstLine = getFirstLineNumber();
		int begin = (firstLine > index) ? firstLine : index;

		for (int i = begin; i < _lines.size(); i++) {
			drawLine(i);
		}

	}

	private char getCharacterAtCursorPosition() {
		char result = 0;
		if (_text.length() > 0) {
			String line = (String) getLine(_cursPosY);
			if (_cursPosX < line.length()) {
				result = line.charAt(_cursPosX);
			} else {
				result = 0;
			}
		}
		return result;
	}

	private int getCurrentPageNumber() {
		return getPageNumber(_cursPosY);
	}

	private int getFirstCharNumber() {
		/*
		 * int result = _cursPosX - getTextRectangle().getWidth()+1; result =
		 * (result < 0)?0:result;
		 */
		return _firstChar;
	}

	// Paging

	private int getFirstLineNumber() {
		/*
		 * int result = _cursPosY - getTextRectangle().getHeight()+1; result =
		 * (result < 0)?0:result;
		 */
		return _firstLine;
	}

	private String getLine(int index) {
		if (index > _lines.size()) {
			return null;
		} else {
			int pos = ((Integer) _lines.get(index)).intValue();
			int length = ((Integer) _lineLengths.get(index)).intValue();

			return _text.substring(pos, pos + length);
		}
	}

	private Rectangle getLineRectangle(int firstLine) {
		Rectangle rect = getTextRectangle();
		int y = ((firstLine - _firstLine) >= 0) ? (firstLine - _firstLine) : 0;
		rect.setLocation(rect.getX(), rect.getY() + y);
		rect.setHeight(1);
		return rect;
	}

	private int getPageNumber(int index) {
		return getPaging().getPageNumber(index);
	}

	private int getPageSize() {
		return getPaging().getPageSize();
	}

	private Paging getPaging() {
		return new Paging(getVisibleHeight(), getTextHeight());
	}

	private Rectangle getTextRectangle(int firstLine) {
		Rectangle rect = getTextRectangle();
		int y = ((firstLine - _firstLine) >= 0) ? (firstLine - _firstLine) : 0;
		rect.setLocation(rect.getX(), rect.getY() + y);
		rect.setHeight(rect.getHeight() - y);
		return rect;
	}

	private int getVisibleHeight() {
		return getTextRectangle().getHeight();
	}

	private void insertCharAtCursorLocation(char c) {
		int pos = ((Integer) _lines.get(_cursPosY)).intValue() + _cursPosX;
		_text.insert(pos, c);
		updateText();
	}

	private boolean isCursorChanged(int x, int y) {
		return !((_cursPosX == x) && (_cursPosY == y));
	}

	private boolean isTextChanged(int x, int y) {
		return !((_firstChar == x) && (_firstLine == y));
	}

	private void redrawAfterCursorMove(int bCursorPosX, int bCursorPosY,
			int bFirstChar, int bFirstLine, char bChar) {
		if (isTextChanged(bFirstChar, bFirstLine)) {
			paint();
		} else if (isCursorChanged(bCursorPosX, bCursorPosY)) {
			redrawOldChar(bCursorPosX, bCursorPosY, bChar);
			drawCursor();
		}
	}

	private void redrawAfterTextChange(int bCursorPosX, int bCursorPosY,
			int bFirstChar, int bFirstLine) {
		if (isTextChanged(bFirstChar, bFirstLine)) {
			paint();
		} else if (isCursorChanged(bCursorPosX, bCursorPosY)) {
			int y = Math.min(_cursPosY, bCursorPosY);
			drawBox(getTextRectangle(y));
			drawText(y);
			drawCursor();
			refreshAdditionalThings();
		}

	}

	private void redrawLine(int index) {
		drawBox(getLineRectangle(index));
		drawLine(index);
		drawCursor();
		refreshAdditionalThings();
	}

	private void redrawOldChar(int bCursorPosX, int bCursorPosY, char bChar) {
		CharColor colors = hasFocus() ? getFocusedTextComponentColors()
				: getColors();
		drawChar(bCursorPosX, bCursorPosY, colors, bChar);

	}

	private void reset() {
		_cursPosX = 0;
		_cursPosY = 0;
		_firstChar = 0;
		_firstLine = 0;
	}

	private void setCursorLocation(int x, int y, boolean pageAlignment) {
		if (y < 0) {
			_cursPosY = 0;
		} else if (y >= _lines.size()) {
			_cursPosY = _lines.size() - 1;
		} else {
			_cursPosY = y;
		}

		int length = ((Integer) _lineLengths.get(_cursPosY)).intValue();

		if (x < 0) {
			_cursPosX = 0;
		} else if (x >= length) {
			_cursPosX = length;
		} else {
			_cursPosX = x;
		}

		_cursPosY = (_cursPosY < 0) ? 0 : _cursPosY;

		// first Position

		if (_firstChar > _cursPosX) {
			_firstChar = _cursPosX;
		} else if (_firstChar < (_cursPosX - getTextRectangle().getWidth() + 1)) {
			_firstChar = (_cursPosX - getTextRectangle().getWidth() + 1);
		}

		if (pageAlignment) {
			_firstLine = getPageStartIndex(getPageNumber(_cursPosY));
		} else {
			if (_firstLine > _cursPosY) {
				_firstLine = _cursPosY;
			} else if (_firstLine < (_cursPosY - getTextRectangle().getHeight() + 1)) {
				_firstLine = (_cursPosY - getTextRectangle().getHeight() + 1);
			}
		}

	}

	private void updateText() {
		updateText(true);
	}

	private void updateText(boolean dispatchEvent) {

		// neu darstellen

		_lines.clear();
		_lineLengths.clear();

		String text = _text.toString();

		int pos = 0;

		while (text.indexOf("\n", pos) != -1) {
			_lines.add(new Integer(pos));
			_lineLengths.add(new Integer(text.indexOf("\n", pos) - pos));
			pos = text.indexOf("\n", pos) + 1;
		}

		if (pos < text.length()) {
			_lines.add(new Integer(pos));
			_lineLengths.add(new Integer(_text.length() - pos));
		}

		if (_lines.size() == 0) {
			_lines.add(new Integer(0));
			_lineLengths.add(new Integer(0));
		} else {
			if (text.endsWith("\n")) {
				_lines.add(new Integer(text.length()));
				_lineLengths.add(new Integer(0));
			}
		}

		// event abschicken

		if (dispatchEvent) {
			_listenerManager.handleEvent(new ValueChangedEvent(this));
		}

	}

	protected void doPaint() {
		drawBox();
		drawText();
		if (hasFocus()) {
			drawCursor();
		}
	}

	protected void doRepaint() {
		doPaint();
	}

	/**
	 * This method draws text-dependent additional things such as scrollbars. As
	 * default it makes nothing, can be overriden in inherited classes ( for
	 * example text area)
	 */
	protected void drawAdditionalThings() {
		// nothing
	}

	/**
	 * @return preferred height
	 */
	protected int getHeight() {
		return _height;
	}

	/**
	 * Aus Widget
	 */
	protected Rectangle getPreferredSize() {
		return new Rectangle(_width, _height);
	}

	protected int getTextHeight() {
		return _lines.size();
	}

	/**
	 * The method returns the rectangle, within that the text is painted. Is
	 * overrided by derived classes for example to implement a border.
	 * 
	 */

	protected Rectangle getTextRectangle() {
		Rectangle result = getSize();
		result.setLocation(getAbsoluteX(), getAbsoluteY());
		result.setHeight(result.getHeight());
		return result;
	}

	// Listener

	protected int getTextWidth() {
		int result = 0;

		for (int i = 0; i < _lineLengths.size(); i++) {
			int value = ((Integer) _lineLengths.get(i)).intValue();
			if (value > result) {
				result = value;
			}
		}

		return result;
	}

	/**
	 * F�r abgeleitete Klassen Textbreite und H�he und Koordinaten der oberen
	 * Rechten Ecke des Sichtbaren Bereiches
	 */

	protected int getTextX() {
		return _firstChar;
	}

	protected int getTextY() {
		return _firstLine;
	}

	/**
	 * @return preferred width
	 */
	protected int getWidth() {
		return _width;
	}

	/**
	 * This method refreshes text-dependent additional after a text change such
	 * as scrollbars. As default it makes nothing, can be overriden in inherited
	 * classes ( for example text area)
	 */
	protected void refreshAdditionalThings() {
		// nothing
	}

	/**
	 * This method replaces a line of text to printing througth another text
	 * line. This can be overrided in derived classes for example for painting
	 * all chars as capitals, or for password input. Here the same line is
	 * returned.
	 * 
	 * @param line
	 *            a text line to replace, contains no line breaks
	 * @return decoded line
	 */

	protected String replaceTextLineForPrinting(String line) {
		return line;
	}

	int getCurrentPageOffset() {
		return getPaging().getPageOffset(_cursPosY);
	}

	int getPageEndIndex(int pageNumber) {
		return getPaging().getPageEndIndex(pageNumber);
	}

	int getPageStartIndex(int pageNumber) {
		return getPaging().getPageStartIndex(pageNumber);
	}

}
